查看原文
其他

Android修炼:Lottie是如何解放了开发的双手?

推荐关注↓

Lottie 是 Airbnb 开发的一套跨平台框架,可以将 AE 生成的动画效果,在各个平台呈现出来,支持 Android、iOS、Web、MacOS、Windows 等。以 Android 为例,设计师使用 AE 设计好动画,通过插件 Bodymovin 将 AE 工程文件导出为 json 文件,app 通过 Lottie 解析出相应的数据结构,最后通过 Canvas 进行绘制。

如果平时自己测试玩,可以直接在 LottieFiles 网站 去下载 json 动画文件,内容丰富。看个栗子吧,效果见下:

此动画效果可通过 AE 导出为 RobotWave.json 文件,详见demo,其格式是这样的:

{
    "v":"4.10.1",       # bodymovin的版本
    "fr":60,            # 动画帧数,这里表示1s执行60
    "ip":0,             # 动画起始帧数
    "op":123,           # op-ip 得到动画的总帧数
    "w":400"h":400,   # 动画的宽和高,Lottie会对手机屏幕进行适配        
    "nm":"AndroidWave", # 名称
    "ddd":0,            # 3d
    "assets":Array[],   # 动画的资源文件
    "layers":Array[]    # 图层信息,要绘制的内容
}

开发者拿到此 json 文件后,怎么使用呢?这里以 lottie-android 为例:

implementation 'com.airbnb.android:lottie:3.7.0'

代码实现

在我们的 xml 文件之内,添加 LottieAnimationView(支持动态创建):

    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/animationView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        airbnb:lottie_fileName="RobotWave.json"
        airbnb:lottie_autoPlay="true"
        airbnb:lottie_loop="true"/>

在我们的代码中,直接使用即可,是不是非常方便:

    private lateinit var lottieAnimaView: LottieAnimationView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.lottie_layout)
        
        lottieAnimaView = findViewById(R.id.animationView)
        lottieAnimaView.playAnimation()
    }

json源

lottie不仅可以加载 assets 文件夹内的 json,同时也支持:

  • src/main/res/raw 中的 json 动画
  • src/main/assets 下的 .zip 文件
  • json 或 zip 文件的 URL
  • 来自任意位置的 json 字符串
  • 来自Json InputStream

这里就不一一列举了,LottieAnimationView 提供了相应 set 方法,也可以使用 LottieCompositionFactory 提供的方法,仅以 url 为例说下:

    private val url = "https://assets1.lottiefiles.com/packages/lf20_gygeywbl.json"
    private var cacheKey : String? = null;
    
    fun playAnimaFromUrl() {
         LottieCompositionFactory.fromUrl(application, url, cacheKey)
                .addListener() {lottieAnimaViewByUrl.setComposition(it)}
        /* lottieAnimaViewByUrl.setAnimationFromUrl(url, cacheKey) */
        lottieAnimaViewByUrl.playAnimation()
    }

效果见下:

向注册监听或基本动画配置等API,就不在这里说了,用到的时候,查看 Lottie官网 即可。下面来说下 Lottie 是如何通过 JSON 让动画动起来的。

动画原理

1、JSON 到 LottieComposition

Lottie的第一步就是去解析 JSON, 将 json 各节点数据映射到 LottieComposition 类的字段内。这里以FromJsonString 为例,首先会将 JSON 转成 InputStream 流,并通过LottieCompositionFactory 这个简单工厂传入:

  public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) {
    setAnimation(new ByteArrayInputStream(jsonString.getBytes()), cacheKey);
  }
  public void setAnimation(InputStream stream, @Nullable String cacheKey) {
    setCompositionTask(LottieCompositionFactory.fromJsonInputStream(stream, cacheKey));
  }

在这里会有一个缓冲读取机制,当无缓存可读时,将stream传入转为同步加载:

  public static LottieTask<LottieComposition> fromJsonInputStream(final InputStream stream, @Nullable final String cacheKey) {
    // 先在缓存中查找,有则直接返回 LottieTask<LottieComposition>
    // 如果没有,则会在 LottieTask 内执行 call() 回调
    return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
      @Override
      public LottieResult<LottieComposition> call() {
        return fromJsonInputStreamSync(stream, cacheKey);
      }
    });
  }

在这里会将 InputStream 流转成 JsonReader 流,这样可以避免一次性全部加载到内存而引起 OOM 问题:

  @WorkerThread
  private static LottieResult<LottieComposition> fromJsonInputStreamSync(InputStream stream, @Nullable String cacheKey, boolean close) {
    return fromJsonReaderSync(JsonReader.of(buffer(source(stream))), cacheKey);
  }
  @WorkerThread
  public static LottieResult<LottieComposition> fromJsonReaderSync(com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey) {
    return fromJsonReaderSyncInternal(reader, cacheKey, true);
  }

最后通过 LottieCompositionMoshiParser.parse 方法从流中解析出完整的 LottieComposition 对象,LottieComposition 就包含了上面介绍的 Json 各节点数据:

  private static LottieResult<LottieComposition> fromJsonReaderSyncInternal(
      com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey, boolean close) 
{
    LottieComposition composition = LottieCompositionMoshiParser.parse(reader);
      if (cacheKey != null) {
        LottieCompositionCache.getInstance().put(cacheKey, composition);
      }
      return new LottieResult<>(composition);
  }

2、BaseLayer 到 LottieComposition

Lottie的第二步就是通过 LottieComposition 去生成各个层级的 BaseLayer,这里 LottieAnimationView 将composition 委托 lottieDrawable 去处理:

  public void setComposition(@NonNull LottieComposition composition) {
    ...
    boolean isNewComposition = lottieDrawable.setComposition(composition); ...
  }
  public boolean setComposition(LottieComposition composition) {
    ...
    buildCompositionLayer(); ...
  }

在这里会创建一个 CompositionLayer 对象,内部管理着各个层级的 Layer:

  private void buildCompositionLayer() {
    compositionLayer = new CompositionLayer(
        this, LayerParser.parse(composition), composition.getLayers(), composition);
    ...
  }

将 composition 内部的 Layer 集合,根据 type 生成对应 BaseLayer 具体类,并保存在 layerMap 集合内:

  public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
      LottieComposition composition) 
{
          LongSparseArray<BaseLayer> layerMap =
        new LongSparseArray<>(composition.getLayers().size());
    ...
    BaseLayer mattedLayer = null;
    for (int i = layerModels.size() - 1; i >= 0; i--) {
      Layer lm = layerModels.get(i);
      BaseLayer layer = BaseLayer.forModel(lm, lottieDrawable, composition);
      if (layer == null) {
        continue;
      }
      layerMap.put(layer.getLayerModel().getId(), layer);
      ...
      } ...
  }

这是具体的 BaseLayer 类转换工具类:

  static BaseLayer forModel(
      Layer layerModel, LottieDrawable drawable, LottieComposition composition) 
{
    switch (layerModel.getLayerType()) {
      case SHAPE:
        return new ShapeLayer(drawable, layerModel);
      case PRE_COMP:
        return new CompositionLayer(drawable, layerModel,
            composition.getPrecomps(layerModel.getRefId()), composition);
      case SOLID:
        return new SolidLayer(drawable, layerModel);
      case IMAGE:
        return new ImageLayer(drawable, layerModel);
      case NULL:
        return new NullLayer(drawable, layerModel);
      case TEXT:
        return new TextLayer(drawable, layerModel);
      case UNKNOWN:
      default:
        // Do nothing
        Logger.warning("Unknown layer type " + layerModel.getLayerType());
        return null;
    }
  }

3、播放动画

当点击播放时,LottieAnimationView 会委托 lottieDrawable,lottieDrawable 又会委托 animator 去执行 playAnimation 方法,animator 是 LottieValueAnimator 的对象,其控制着整个动画的进度和更新:

  @MainThread
  public void playAnimation() {
    if (isShown()) {
      lottieDrawable.playAnimation();
    } ...
  }
  public void playAnimation() {
    ...
    if (animationsEnabled() || getRepeatCount() == 0) {
      animator.playAnimation();
    } ...
  }

notifyStart 方法会通知动画开始。setFrame 方法来设置当前帧数据,postFrameCallback 方法是核心,具体见下:

  @MainThread
  public void playAnimation() {
    running = true;
    // notifyStart 会通知 listener.onAnimationStart(this, isReverse);
    notifyStart(isReversed());
    setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
    lastFrameTimeNs = 0;
    repeatCount = 0;
    postFrameCallback();
  }

postFrameCallback 方法,会请求 VSYNC 信号:

  protected void postFrameCallback() {
    if (isRunning()) {
      removeFrameCallback(false);
      Choreographer.getInstance().postFrameCallback(this);
    }
  }

之后会回调到 LottieValueAnimator 内部的 doFrame 方法,我们能看到此时又会执行 postFrameCallback 方法,而 VSYNC 信号的间隔是是16.6ms,所以此方法会每过 16.6ms 就会更新下 frame 的值,并调用 notifyUpdate 通知 LottieDrawable 的 onAnimationUpdate 回调:

  @Override public void doFrame(long frameTimeNanos) {
    postFrameCallback();
    if (composition == null || !isRunning()) {
      return;
    }

    L.beginSection("LottieValueAnimator#doFrame");
    long now = frameTimeNanos;
    long timeSinceFrame = lastFrameTimeNs == 0 ? 0 : now - lastFrameTimeNs;
    float frameDuration = getFrameDurationNs();
    float dFrames = timeSinceFrame / frameDuration;

    frame += isReversed() ? -dFrames : dFrames;
    boolean ended = !MiscUtils.contains(frame, getMinFrame(), getMaxFrame());
    frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());

    lastFrameTimeNs = now;

    notifyUpdate();
    ...
  }
  
  void notifyUpdate() {
    for (ValueAnimator.AnimatorUpdateListener listener : updateListeners) {
      listener.onAnimationUpdate(this);
    }
  }

LottieDrawable 的 onAnimationUpdate 回调后,会更新进度值:

  private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
      if (compositionLayer != null) {
        compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
      }
    }
  };

LottieDrawable 通知每个 layers 去更新自己的进度值:

  @Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    super.setProgress(progress);
    ...
    for (int i = layers.size() - 1; i >= 0; i--) {
      layers.get(i).setProgress(progress);
    }
  }

BaseLayer 更新进度值后,会通知 onValueChanged :

  void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    // Time stretch should not be applied to the layer transform.
    transform.setProgress(progress);
    ...
    if (matteLayer != null) {
      // The matte layer's time stretch is pre-calculated.
      float matteTimeStretch = matteLayer.layerModel.getTimeStretch();
      matteLayer.setProgress(progress * matteTimeStretch);
    }
    for (int i = 0; i < animations.size(); i++) {
      animations.get(i).setProgress(progress);
    }
  }
  
  public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    ...
    this.progress = progress;
    if (keyframesWrapper.isValueChanged(progress)) {
      notifyListeners();
    }
  }
  
  public void notifyListeners() {
    for (int i = 0; i < listeners.size(); i++) {
      listeners.get(i).onValueChanged();
    }
  }

BaseLayer 随后会通知 lottieDrawable 去更新,之后会触发 LottieDrawable 的绘制方法 draw:

  /* BaseLayer.java */
  @Override public void onValueChanged() {
    invalidateSelf();
  }
  
  private void invalidateSelf() {
    lottieDrawable.invalidateSelf();
  }
  
  /* LottieDrawable.java*/
  @Override public void draw(@NonNull Canvas canvas) {
   if (safeMode) {
      try {
        drawInternal(canvas);
      } catch (Throwable e) {
        Logger.error("Lottie crashed in draw!", e);
      }
    } ...
  }

LottieDrawable 之后又会去通知 compositionLayer 去绘制:

  private void drawInternal(@NonNull Canvas canvas) {
    if (!boundsMatchesCompositionAspectRatio()) {
      drawWithNewAspectRatio(canvas);
    } else {
      drawWithOriginalAspectRatio(canvas);
    }
  }
  
  private void drawWithOriginalAspectRatio(Canvas canvas) {
    ...
    compositionLayer.draw(canvas, matrix, alpha);
    ...
  }

最后 compositionLayer 又会去通知每个 layer,至此完成所有绘制工作。

  public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    ...
    if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
      drawLayer(canvas, matrix, alpha);
      return;
    }
  ...
  }


转自:掘金 Battler

https://juejin.cn/post/6972947515641430024

- EOF -

推荐阅读  点击标题可跳转

1、组件化开花,就问你香不香

2、一道面试题:Glide 做了哪些优化?

3、公司到底能不能监控到微信聊天?


看完本文有收获?请分享给更多人

 推荐关注「安卓开发精选」,提升安卓开发技术

点赞和在看就是最大的支持❤️

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存